iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
自我挑戰組

React 入門到實作與除錯|30 天哩ㄟ刻系列 第 6

Day 6 : 一五一十,陣列與 key

  • 分享至 

  • xImage
  •  

一萬,兩萬,三萬...哎呀,那是老闆的錢

今天目標

  1. 資料整理成陣列
  2. 認識 map()、filter()、key 與 unique
  3. 測試執行
  4. 錯誤修正練習

舉凡像是條列商品頁、演唱會座位、輪替顯示等,如果每個都刻在程式內,那會累死人的。

今天來修改卡片的資料,使用陣列與 key 來處理,一樣只修改 welcome.tsx (會不會用到最後還在這裡 /images/emoticon/emoticon37.gif)

資料整理成陣列

陣列

陣列(Array)的認識,看下面示範更清楚

const colors = ["red", "yellow", "blue"];

上面宣告一組陣列,放三個字串元素,每個元素還會被分到一個 index (索引值),由左到右分別是:

0: "red"
1: "yellow"
2: "blue"

最基本的認識就是每個元素會有一個索引值,索引值可以找到對應的元素,例如:

const selectedColor = colors[1]; // 這裡通過索引值 1 選擇到 yellow

更多認識可以看Array

物件

物件(Object)可以用來承載資料的屬性,例如:

王小明同學
學習卡 ID:"ming-react"
標題:"我的 React 練習"
主題:"清單與 key"
學習進度:6 天
挑戰總天數:30 天
今日筆記:"今天讓資料排成清單"

將以上資訊轉成物件,變成:

const ming = {
    id: "ming-react",
    title: "我的 React 練習",
    topic: "清單與 key",
    day: 6,
    totalDays: 30,
    note: "今天讓資料排成清單"
}

型別、類別

除了物件,還有型別(Type)與類別(Class),看下面的整理表格有個概念;

名稱 用途 範例
物件(object) 程式執行時,實際存放屬性和值的資料,也可以包含方法 陣列裡的元素 { id: "ming-react", ... }
型別(type) 描述資料的結構,讓 TypeScript 檢查 下方完整範例的 type LearningPlan = { ... }
類別(class) 定義物件的建立方式、屬性和方法,可以用 new 建立實例 下方的 LearningProgress

因為 TypeScript 可以對資料屬性定義嚴格的檢查,也比較不容易出錯。

型別

例如以下用法是天數的簡化範例,示範物件和型別怎麼配合:

type ProgressData = { //這裡宣告ProgressData,並使用 (type) 來描述資料結構,下方屬性定資料型別
  day: number; // 像是 day 只能為 number
  totalDays: number;
};

const progress: ProgressData = {
  day: 6,
  totalDays: 30,
};

之後還會看到 interface,它也可以描述物件的結構。這裡先使用 type,兩者的進階差異留到需要時再深入。ObjectType Aliases 與 Interfaces

類別

如果想把「建立進度資料」和「判斷是否完成」一起整理,也可以寫成類別,例如:

class LearningProgress { // 定義類別,名稱為 LearningProgress
  day: number;
  totalDays: number;

  constructor(day: number, totalDays: number) { // 建構子,使用 new 的時候會執行
    this.day = day;
    this.totalDays = totalDays;
  }

  isCompleted(): boolean { // 成員函式,可以呼叫使用
    return this.day >= this.totalDays;
  }
}

const myProgress = new LearningProgress(6, 30);
const mingProgress = new LearningProgress(30, 30);

console.log(myProgress.day); // 6
console.log(myProgress.isCompleted()); // false
console.log(mingProgress.isCompleted()); // true

constructor 是建構子,使用 new 建立物件時會執行,用來設定初始資料。這裡的 this 指向正在建立的那個物件,所以 this.day = day 是把傳進來的天數存到它自己的屬性。

myProgressmingProgress 是同一個類別建立的兩個實例,各自保存自己的天數。isCompleted() 是方法,會讀取呼叫它的那個物件資料;後面的 : boolean 表示回傳布林值。TypeScript:Classes

使用 TypeScript 不代表一定要使用 class。 class 本來就是 JavaScript 的語法,TypeScript 在它的基礎上提供型別檢查等能力。typeinterface 的宣告在轉成 JavaScript 時會移除;類別的建構子和方法則是執行時的程式。(之後再講 interface ,又挖坑)

資料整理

昨天 welcome.tsx 裡放了三個卡片,每個都各有自己的資料,全部提取出來。

範例:

type LearningPlan = { // 建一個型別,名稱是 LearningPlan,底下資料訂定資料名稱和屬性
  id: string;
  title: string;
  topic: string;
  day: number;
  totalDays: number;
  note: string;
};

const learningPlans : LearningPlan[] = [ // 建一組陣列,名稱是 learningPlans
  {
    id: "my-react",
    title: "我的 React 練習",
    topic: "清單與 key",
    day: 6,
    totalDays: 30,
    note: "今天讓資料自己排成清單。",
  },
  {
    id: "ming-react",
    title: "小明的 React 練習",
    topic: "總複習",
    day: 30,
    totalDays: 30,
    note: "完成之後,也可以回頭看看筆記。",
  },
];

這個需求相對簡單,用一般物件搭配型別就可以很清楚表達了。上面可以這樣看,建一組陣列 learningPlans,裡面放元素,元素的型別是 LearningPlan

接著我們看 map()filter() 怎麼用。

認識 map()、filter()、key 與 unique

建立好陣列,我們可以來用陣列提供的函式,map()filter()

map()

翻譯應該是"映射"之類的吧,概念就是一個對一個那樣? 我自己是這樣理解/images/emoticon/emoticon07.gif

以標題為範例:

<ul>
  {learningPlans.map((plan) => ( // 這裡的 "=>" 就是 Arrow Function 的用法
    <li key={plan.id}>{plan.title}</li>
  ))}
</ul>

map() 會依序拿出每筆資料,交給我們提供的函式,再把每次回傳的結果組成新陣列。這裡回傳的是 JSX,所以得到一組清單項目。MDN:Array.prototype.map()

plan 是我們替「這次拿到的那一筆資料」取的參數名稱,plan.title 就是讀取它的標題。=> 是箭頭函式(Arrow Function)的寫法;這裡用 (...) 包住要回傳的 JSX。

ul 表示清單,裡面每個 li 表示一個項目。完整版本會把 LearningCard 放進 li,讓每筆資料變成一張卡片

有點抽象,畫成圖好了,看下面的圖。
https://ithelp.ithome.com.tw/upload/images/20260920/20184332R6W8UW4nCH.png

從圖中就可以理解map()主要將陣列裡的元素依序取出,送到裡面的函式處理,函式會將資料跟 HTML Tag 結合,回傳給 map(),而 React 會顯示這些項目,是因為陣列被放進元件回傳的 JSX 裡。

認識 key、unique

上面可以注意到 key={plan.id} ,這是讓 React 對應清單項目的識別資訊。把它放在 map() 直接回傳到的外層元素上

同一層清單的 key唯一(unique),同一筆資料在後續渲染時也要保持穩定。它不負責排序,畫面順序仍然跟著陣列走。React:Rendering Lists

為什麼知道用哪個屬性當 key 呢,下面的範例表格說明:

寫法 意義
key={plan.id} 使用資料裡事先準備好的識別碼,假設 id 不重複
key={index} 陣列索引代表位置,新增、刪除或換位置時,可能對應到別筆資料
key={plan.title} 可能會改標題,也可能同名,不適合拿來識別
key={Math.random()} 每次渲染都重新產生,無法穩定對應同一個項目

key 也不會成為子元件收到的一般 prop。卡片如果需要顯示識別碼,要另外傳入,例如 planId={plan.id},並在自己的 props 型別裡定義 planId,更多可以參考這篇:React:Why does React need keys?

認識 filter()

如果只想看已完成的計畫,可以用 filter() 篩選:

const completedPlans = learningPlans.filter(
  (plan) => plan.day >= plan.totalDays,
);

filter() 會把每筆資料交給判斷函式,留下結果為 true 的項目,回傳新陣列。這裡使用昨天的完成條件 day >= totalDaysMDN:Array.prototype.filter()

使用順序會是:先 filter() 挑資料,再用 map() 產生 JSX。
這兩個都是 JavaScript 陣列有提供的方法。

注意:篩選出來的陣列不會把原本的項目刪掉。

組合起來

welcome.tsx 替換成下面的版本。(有越來越長的趨勢了/images/emoticon/emoticon16.gif)

import type { ReactNode } from "react";

type LearningPlan = { // 建型別,名稱是 LearningPlan,底下資料訂定資料名稱和屬性
  id: string;
  title: string;
  topic: string;
  day: number;
  totalDays: number;
  note: string;
};

type LearningCardProps = { // 之前的 props
  title: string;
  topic: string;
  day: number;
  totalDays?: number;
  children?: ReactNode;
};

const learningPlans: LearningPlan[] = [ // 建陣列,名稱是 learningPlans,型別是LearningPlan
  {
    id: "my-react",
    title: "我的 React 練習",
    topic: "清單與 key",
    day: 6,
    totalDays: 30,
    note: "今天讓資料自己排成清單。",
  },
  {
    id: "ming-react",
    title: "小明的 React 練習",
    topic: "總複習",
    day: 30,
    totalDays: 30,
    note: "完成之後,也可以回頭看看筆記。",
  },
  {
    id: "mei-react",
    title: "小美的短期練習",
    topic: "元件",
    day: 2,
    totalDays: 7,
    note: "元件可以重複使用,資料各自準備。",
  },
];

export function Welcome() {
  const showCompletedOnly = false; // 加一個變數,用來調整是否要顯示已完成的計劃
  const visiblePlans = showCompletedOnly
    ? learningPlans.filter((plan) => plan.day >= plan.totalDays)
    : learningPlans;
  const emptyMessage = learningPlans.length === 0
    ? "目前沒有學習資料,先安排一個主題吧。"
    : "目前沒有符合條件的學習計畫。";

  return (
    <main className="min-h-screen bg-gray-100 px-6 py-10 text-gray-900">
      <div className="mx-auto max-w-xl space-y-6">
        <h1 className="text-2xl font-bold">我的 React 學習卡片</h1>
        <p>
          {showCompletedOnly ? "只看已完成" : "顯示全部"}:
          {visiblePlans.length} / {learningPlans.length} 筆
        </p>

        {visiblePlans.length === 0 ? ( // '==='用來檢查值與型別
          <p className="rounded-xl bg-white p-6 shadow">{emptyMessage}</p>
        ) : (
          <ul className="space-y-6">
            {visiblePlans.map((plan) => ( // map 把元素逐個轉換成 JSX
              <li key={plan.id}>
                <LearningCard
                  title={plan.title}
                  topic={plan.topic}
                  day={plan.day}
                  totalDays={plan.totalDays}
                >
                  <p className="text-gray-600">{plan.note}</p>
                </LearningCard>
              </li>
            ))}
          </ul>
        )}
      </div>
    </main>
  );
}

function LearningCard({
  title,
  topic,
  day,
  totalDays = 30,
  children,
}: LearningCardProps) {
  const remainingDays = totalDays - day;
  const isCompleted = day >= totalDays;

  return (
    <section className="space-y-4 rounded-xl bg-white p-6 shadow">
      <h2 className="text-xl font-bold">{title}</h2>
      <p className="text-gray-600">今天練習 {topic}</p>
      <hr className="border-gray-200" />
      <h3 className="font-bold text-blue-800">學習進度</h3>
      <p>
        目前進度:第 {day} 天 / 共 {totalDays} 天
      </p>
      <p className="font-bold">{isCompleted ? "已完成" : "還在努力"}</p>
      {remainingDays > 0 && <p>接下來還有 {remainingDays} 天</p>}
      {children}
    </section>
  );
}

LearningPlan[] 表示這是一個每筆都符合 LearningPlan 型別的陣列。它描述資料有哪些欄位,LearningCardProps 則描述卡片接收哪些內容。

卡片仍然保留昨天的完成判斷、預設總天數與 children。今天把三張卡片的最後一塊都改成筆記,透過 children 傳入;原本尚未設定事件的按鈕先換成文字。

昨天的 hasCards 就可以拿掉了。現在是直接檢查真正的資料筆數,而且能區分「原本就沒有資料」和「有資料,但篩選後沒有符合條件的項目」。

測試執行

全部三筆
https://ithelp.ithome.com.tw/upload/images/20260920/20184332ylnS3e9tjK.png

原始資料為空
https://ithelp.ithome.com.tw/upload/images/20260920/20184332QtZkxrAXSY.png

錯誤修正練習

換成大括號,怎麼沒有回傳了?

範例 {visiblePlans.map(...)}

{visiblePlans.map((plan) => {
  <li key={plan.id}>{plan.title}</li>;
})}

這是因為箭頭函式用大括號 {} 作為函式本體時,需要自己寫 return

這個函式有寫 JSX,卻沒有回傳。map() 會得到一組 undefined,不會產生想要的清單。在 TypeScript 專案執行型別檢查時,也會指出 void[] 無法當作可渲染內容。

補上 return,或改回原本 => (...) 的寫法就可以了。

{visiblePlans.map((plan) => {
  return <li key={plan.id}>{plan.title}</li>;
})}

filter() 也要注意同一件事。

範例示意:

? learningPlans.filter((plan) => { plan.day >= plan.totalDays; })

這段沒有回傳判斷結果。設為只看已完成時,所有資料都會被排除,型別檢查卻可能仍然通過,補 return 就OK了。

? learningPlans.filter((plan) => { return plan.day >= plan.totalDays; })

今天把資料整理好,新增卡片終於不用一直複製 JSX。客戶要幾筆資料,就有幾筆。希望我的薪水也可以這樣/images/emoticon/emoticon02.gif

明天再來認識元件為什麼要保持純粹,元件在產生畫面時,如果順手改了外面的資料,會發生什麼事吧。

參考資料


上一篇
Day 5 : 見機行事,條件判斷決定顯示內容
下一篇
Day 7 : 專精一思,讓元件保持純粹
系列文
React 入門到實作與除錯|30 天哩ㄟ刻9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言